跳到主要内容

TypeScript基本数据类型、泛型、数组、断言

前置准备

TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准,其是由微软开发的自由和开源的编程语言;它的设计目标是开发大型应用,就是将 JavaScript 再封装一层,使之拥有更加完善的静态类型机制,通过编译出来的 JavaScript 运行在任何浏览器上(通过 TypeScript 编译器或 Babel 转译为 JavaScript 代码)

参考资料

TypeScript 官网 在线编辑环境 TypeScript 入门教程 菜鸟教程 TypeScript教程 TypeScript 中文文档

注:这篇文章基本照搬上面几个资料的,复制下来是因为有些地方想补充,但是缺乏上下文,所以索性直接都复制下来方便看

搭建环境

npm install -g typescript
# 检测是否安装好
tsc -v

创建一个项目

mkdir typescript-demo
npm init -y # 创建一个 package.json
tsc --init # 创建一个 tsconfig.json
touch index.html
touch hello.ts

编译 TS 文件

tsc hello.ts

因为 TS 是建立在 JS 的基础之上的,但是 NodeJS 又不能直接运行 TS 代码,实际使用是往往需要使用 tsc 将 TS 代码编译成 JS 代码。

所以一般使用的是 ts-node 这个工具

npm install -g ts-node

然后直接使用 ts-node 工具就能运行了

ts-node hello.ts

编写 Debug 的配置文件

{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node", // 这里要使用这个 pwa-node 默认的 node 有点问题
"request": "launch",
"name": "TypeScript Debug",
"cwd": "${workspaceRoot}",
"sourceMaps": true,
"runtimeArgs": [
"-r",
"D:/NodeJSTools/node_global/node_modules/ts-node/register"
],
"args": [
"${relativeFile}"
]
}
],
"resolveSourceMapLocations": [
"${workspaceFolder}/**",
"!**/node_modules/**" // 排除这个目录
],
}

TS 配置文件

参考资料 tsconfig.json

如果一个目录下存在一个 tsconfig.json 文件,那么它意味着这个目录是 TypeScript 项目的根目录。

使用 tsconfig.json

  • 不带任何输入文件的情况下调用 tsc,编译器会从当前目录开始去查找 tsconfig.json 文件,逐级向上搜索父目录。
  • 不带任何输入文件的情况下调用 tsc,且使用命令行参数 --project(或-p)指定一个包含 tsconfig.json 文件的目录。

配置详情

{
"compilerOptions": {
"module": "system",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"outDir": "./dist",
"sourceMap": true
},
"files": [
"hello.ts"
],
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}

"files" 指定一个包含相对或绝对文件路径的列表。 "include""exclude" 属性指定一个文件 glob 匹配模式列表。 支持的 glob 通配符有:

  • * 匹配0或多个字符(不包括目录分隔符)
  • ? 匹配一个任意字符(不包括目录分隔符)
  • **/ 递归匹配任意子目录

数据类型

参考资料 原始数据类型(大部分照搬这篇教程的,主要是这种基础向的东西也没啥好说的,默默做个笔记就行了 (~ ̄▽ ̄)~

JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。

原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol 和 BigInt

类型转换

let msg: any = '这是一串字符串'
let num : number = (<string>msg).length
let num2 : number = (msg as string).length

布尔值 boolean

布尔值是最基础的数据类型,在 TypeScript 中,使用 boolean 定义布尔值类型:

let isDone: boolean = false;

// 编译通过
// 后面约定,未强调编译错误的代码片段,默认为编译通过

注意,使用构造函数 Boolean 创造的对象不是布尔值:

// 注意两个 boolean 大小写不同
let createdByNewBoolean: boolean = new Boolean(1);

// Type 'Boolean' is not assignable to type 'boolean'.
// 'boolean' is a primitive, but 'Boolean' is a wrapper object. Prefer using 'boolean' when possible.

事实上 new Boolean() 返回的是一个 Boolean 对象:(注意这里的大小写,就像 Java 一样)

let createdByNewBoolean: Boolean = new Boolean(1);

直接调用 Boolean 也可以返回一个 boolean 类型:

let createdByBoolean: boolean = Boolean(1);

在 TypeScript 中,boolean 是 JavaScript 中的基本类型,而 Boolean 是 JavaScript 中的构造函数。其他基本类型(除了 nullundefined)一样,不再赘述。

数值 number

使用 number 定义数值类型:

// 其中 0b1010 和 0o744 是 ES6 中的二进制和八进制表示法,它们会被编译为十进制数字。

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

编译结果:

var decLiteral = 6;
var hexLiteral = 0xf00d;
// ES6 中的二进制表示法
var binaryLiteral = 10;
// ES6 中的八进制表示法
var octalLiteral = 484;
var notANumber = NaN;
var infinityNumber = Infinity;

字符串 string

使用 string 定义字符串类型:

let myName: string = 'Tom';
let myAge: number = 25;

// 模板字符串
let sentence: string = `Hello, my name is ${myName}.
I'll be ${myAge + 1} years old next month.`;

编译结果:

var myName = 'Tom';
var myAge = 25;
// 模板字符串
var sentence = "Hello, my name is " + myName + ".\nI'll be " + (myAge + 1) + " years old next month.";

空值 void

JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数:

function alertName(): void {
alert('My name is Tom');
}

声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefinednull

let unusable: void = undefined;

Null 和 Undefined

在 TypeScript 中,可以使用 nullundefined 来定义这两个原始数据类型:

let u: undefined = undefined;
let n: null = null;

void 的区别是,undefinednull 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:

// 这样不会报错
let num: number = undefined;
// 这样也不会报错
let u: undefined;
let num: number = u;

void 类型的变量不能赋值给 number 类型的变量:

let u: void;
let num: number = u;

// Type 'void' is not assignable to type 'number'.

泛型 Any

任意值(Any)用来表示允许赋值为任意类型。

如果是一个普通类型,在赋值过程中改变类型是不被允许的:

let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

但如果是 any 类型,则允许被赋值为任意类型。

let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

这个没什么好说的,就是直接变回 JS 那样弱类型

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

// 如果不是这两种的一个还是会报错
let myFavoriteNumber: string | number;
myFavoriteNumber = true;

// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
// Type 'boolean' is not assignable to type 'number'.

联合类型使用 | 分隔每个类型

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:

function getLength(something: string | number): number {
return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.

上例中,length 不是 stringnumber 的共有属性,所以会报错。

访问 stringnumber 的共有属性是没问题的:

function getString(something: string | number): string {
return something.toString();
}

元组类型

数组中元素的数据类型都一般是相同的(any[] 类型的数组可以不同),如果存储的元素数据类型不同,则需要使用元组。元组中允许存储不同类型的元素,元组可以作为参数传递给函数。

语法如下

let tuple_name = [value1,value2,value3,…value n]

使用例

let tom  = ['Tom', 25];
tom.push(2) // 向元组添加元素,添加在最后面。
tom.push(3)
tom.push(4)
tom.push(5)
tom.push(6)
tom.push(7)
console.log(tom);


tom.pop() // 从元组中移除元素(最后一个),并返回移除的元素。
console.log(tom[0]); // 取得第一个元素

console.log("取得元组的长度" + tom.length)

// 更新元组元素
tom[0] = 12
tom[4] = true // 报错,因为上面只用到了两种类型 string 和 number 类型,如果要赋值 Boolean 类型必须上面也用到

也有指定类型的写法,但是这种方法就失去了上面的那种拓展性:如下定义一对值分别为 stringnumber 的元组

let tom: [string, number] = ['Tom', 25]; 
tom.push(2) // 依然可以添加元素,但是这里添加的元素实际是 undefined 类型
tom.push(3)
tom.push(4)
tom.push(5)
tom.push(6)
tom.push(7)
console.log(tom);


tom.pop() // 从元组中移除元素(最后一个),并返回移除的元素。
console.log(tom[0]); // 取得第一个元素

console.log("取得元组的长度" + tom.length)

// 更新元组元素(但是初始化时界定了类型的就无法更新为其它类型)
tom[0] = 12 // 报错,因为是 string 类型
tom[3] = 12 // 报错,因为初始化时只界定了两个元素,后面添加的元素实际是 undefined 类型
tom[4] = true

枚举类型

enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };

let work: Days = Days.Mon // 赋值

console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:(这点有点像 C#)

如果需要也可以使用手动赋值

enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

// 如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的:
enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 3); // true
console.log(Days["Wed"] === 3); // true
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true

泛型 Generics

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

定义泛型的时候,可以一次定义多个类型参数:

function swap<T, U>(tuple: [T, U]): [U, T] {
console.log(typeof tuple[0]);
console.log(typeof tuple[1]);

return [tuple[1], tuple[0]];
}

swap([7, 'seven']);
// number
// string
// ['seven', 7]

泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

interface Lengthwise {
length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}

上例中,我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。

多个类型参数之间也可以互相约束:(例如这里的 其中要求 T 继承 U

function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 });

泛型接口

interface CreateArrayFunc<T> {
(length: number, value: T): Array<T>;
}


// 此时在使用泛型接口的时候,需要定义泛型的类型。
let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

泛型类

class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zeroValue: T, add: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = add;
}
}

let myGenericNumber = new GenericNumber<number>(18, (x, y) => {
return x + y
});

数组的类型

在 TypeScript 中,数组类型有多种定义方式,比较灵活。

经典表示法

最简单的方法是使用「类型 + 方括号」来表示数组:

let fibonacci: number[] = [1, 1, 2, 3, 5];

数组的项中不允许出现其他的类型:

let fibonacci: number[] = [1, '1', 2, 3, 5];

// Type 'string' is not assignable to type 'number'.

数组的一些方法的参数也会根据数组在定义时约定的类型进行限制:

let fibonacci: number[] = [1, 1, 2, 3, 5];
fibonacci.push('8');

// Argument of type '"8"' is not assignable to parameter of type 'number'.

上例中,push 方法只允许传入 number 类型的参数,但是却传了一个 "8" 类型的参数,所以报错了。这里 "8" 是一个字符串字面量类型,会在后续章节中详细介绍。

数组泛型

我们也可以使用数组泛型(Array Generic)来表示数组:

let fibonacci: Array<number> = [1, 1, 2, 3, 5];

any 在数组中的应用

一个比较常见的做法是,用 any 表示数组中允许出现任意类型:

let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];

类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。

as 类型
// 或
<类型>

不过类型的形式还可能表示一个泛型,所以最好使用 值 as 类型

类型断言的常见用途有以下几种:

1、将一个联合类型断言为其中一个类型(当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,只能访问此联合类型的所有类型中共有的属性或方法)

interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}

// 需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}

原理就是 (animal as Fish).swim() 这段代码隐藏了 animal 可能为 Cat 的情况,将 animal 直接断言为 Fish 了,而 TypeScript 编译器信任了我们的断言,故在调用 swim() 时没有编译错误。

可是 swim 函数接受的参数是 Cat | Fish,一旦传入的参数是 Cat 类型的变量,由于 Cat 上没有 swim 方法,就会导致运行时错误了。

总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。

2、将一个父类断言为更加具体的子类

interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}

function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}

3、将任何一个类型断言为 any

const foo: number = 1;

window.foo = 1; // 这时会报错,所以需要将其断言成 any
(window as any).foo = 1;

4、将 any 断言为一个具体的类型 举例来说,历史遗留的代码中有个 getCacheData,它的返回值是 any

function getCacheData(key: string): any {
return (window as any).cache[key];
}

interface Cat {
name: string;
run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();